﻿// The MIT License(MIT)
//
// Copyright(c) 2021 Alberto Rodriguez Orozco & LiveCharts Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System.Text;
using LiveChartsGenerators.Definitions;
using LiveChartsGenerators.Frameworks;
using Microsoft.CodeAnalysis;

namespace LiveChartsGenerators.Templates;

public static class XamlObjectTempaltes
{
    public static string GetTemplate(XamlObject target, FrameworkTemplate template)
    {
        var baseType = target.BasedOn.OriginalDefinition.ToDisplayString();

        return @$"// <auto-generated>
//     This code was generated by a LiveCharts source generator, do not edit.
// </auto-generated>

#nullable enable
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required
#pragma warning disable CS0618 // ignore obsolete member
#pragma warning disable IDE0052 // Remove unread private members
#pragma warning disable IDE1006 // Naming Styles
{target.FileHeader}
using LiveChartsCore.SkiaSharpView.TypeConverters;

namespace {target.NameSpace};

/// <summary>
/// A <see cref=""{baseType.Replace('<', '{').Replace('>', '}')}""/> instance for XAML.
/// </summary>
public partial class {target.Name} : LiveChartsCore.Generators.IXamlWrapper<{baseType}>
{{
    {(target.GenerateBaseTypeDeclaration ? $"private readonly {baseType} _baseType = new();" : string.Empty)}
    {(target.IsSeries ? "protected override ISeries WrappedSeries => _baseType;" : string.Empty)}

#region default values

{(target.GenerateBaseTypeDeclaration ? GetDefaultValues(target) : string.Empty)}
#endregion

#region properties

    {baseType} LiveChartsCore.Generators.IXamlWrapper<{baseType}>.WrappedObject => _baseType;
{Concatenate(target.NotBindableProperties, GetRegularPropertySyntax)}
#endregion

#region bindable properties

{Concatenate(
    target.BindableProperties,
    pair => Concatenate(pair.Value, property => template.GetBindablePropertySyntax(target, pair.Key, property)))}
#endregion

#region events

{Concatenate(target.Events, e => GetEventSyntax(template, target, e))}
#endregion

#region methods

{Concatenate(target.Methods, GetMethodSyntax)}
#endregion

#region explicit methods

{Concatenate(target.ExplicitMethods, GetExplicitMethodSyntax)}
#endregion

    {(target.ManualOnPropertyChanged ? string.Empty : template.GetPropertyChangedMetod())}

    private System.Collections.Generic.HashSet<string> _setCommands = [];

    private void MapChangeToBaseType(string? propertyName = null)
    {{
        switch (propertyName)
        {{
{GetChangesMap(target, template)}{GetCommandsChangesMap(target)}
            default:
                break;
        }}
    }}

    static {target.Type.Name}()
    {{
        {Concatenate(
    target.BindableProperties,
    pair => Concatenate(pair.Value, property => @$"{GetPropertyName(target, property)}Property = {template.GetBindablePropertyDefinition(target, property, pair.Key)}
        ", false),
    false)}
    }}
}}{(target.IsSeries ? GetSeriesExtraGeneration(target) : string.Empty)}";
    }

    private static string GetDefaultValues(XamlObject target)
    {
        var sb = new StringBuilder();

        var fallBackInfo = FrameworkTemplate.GetFallbackInfo(target.BasedOn);

        _ = sb.AppendLine(@$"    // this object is used to set the default value of bindable properties.
    private static readonly {fallBackInfo.Item1} {fallBackInfo.Item2} = new();");

        return sb.ToString();
    }

    private static string GetRegularPropertySyntax(IPropertySymbol property)
    {
        var propertyName = property.Name;
        var propertyType = property.Type.ToDisplayString();

        var propertyParts = propertyName.Split('.');

        // exception and special case for ChartElement
        if (propertyName.EndsWith("ChartElement"))
            return $"    public LiveChartsCore.Kernel.ChartElement ChartElement => _baseType;";

        if (propertyParts.Length > 1)
        {
            // this is an explicitly implemented property
            var path = string.Join(".", propertyParts, 0, propertyParts.Length - 1);
            var name = propertyParts[propertyParts.Length - 1];

            return property.SetMethod is null
                ? $"    {propertyType} {propertyName} => (({path})_baseType).{name};"
                : $"    {propertyType} {propertyName} {{ get => (({path})_baseType).{name}; set => (({path})_baseType).{name} = value; }}";
        }

        return property.SetMethod is null
            ? $"    public {propertyType} {propertyName} => _baseType.{propertyName};"
            : @$"    public {propertyType} {propertyName} {{ get => throw new System.NotImplementedException(""The generator was not able to generate the property syntax.""); set => throw new System.NotImplementedException(""The generator was not able to generate the property syntax.""); }}";
    }

    private static string GetEventSyntax(FrameworkTemplate template, XamlObject target, IEventSymbol @event) =>
        @$"    public new event {@event.Type.ToDisplayString()} {@event.Name}
    {{
        add => _baseType.{@event.Name} += value;
        remove => _baseType.{@event.Name} -= value;
    }}
{template.GetBindablePropertySyntax(target, "_baseType", $"{@event.Name}Command", "System.Windows.Input.ICommand?", false, new(target.Name, "null"))}";

    private static string GetMethodSyntax(IMethodSymbol method)
    {
        var sb = new StringBuilder();
        var sb1 = new StringBuilder();
        var isFirst = true;

        foreach (var p in method.Parameters)
        {
            if (!isFirst)
            {
                _ = sb.Append(", ");
                _ = sb1.Append(", ");
            }

            _ = sb.Append($"{p.Type.ToDisplayString()} {p.Name}");
            _ = sb1.Append(p.Name);

            isFirst = false;
        }

        return @$"    public {method.ReturnType} {method.Name}({sb}) => _baseType.{method.Name}({sb1});";
    }

    private static string GetExplicitMethodSyntax(IMethodSymbol method)
    {
        var sb = new StringBuilder();
        var sb1 = new StringBuilder();
        var isFirst = true;

        foreach (var p in method.Parameters)
        {
            if (!isFirst)
            {
                _ = sb.Append(", ");
                _ = sb1.Append(", ");
            }

            _ = sb.Append($"{p.Type.ToDisplayString()} {p.Name}");
            _ = sb1.Append(p.Name);

            isFirst = false;
        }

        var nameSplit = method.Name.Split('.');
        var path = string.Join(".", nameSplit, 0, nameSplit.Length - 1);
        var actualName = nameSplit[nameSplit.Length - 1];

        return @$"    {method.ReturnType} {method.Name}({sb}) => (({path})_baseType).{actualName}({sb1});";
    }

    private static string GetChangesMap(XamlObject target, FrameworkTemplate template)
    {
        var sb = new StringBuilder();

        foreach (var pair in target.BindableProperties)
        {
            var path = pair.Key;
            if (pair.Key.Length == 0)
                path = "_baseType";

            foreach (var property in pair.Value)
            {
                var hasPublicSetter = property.SetMethod is not null && property.SetMethod.DeclaredAccessibility == Accessibility.Public;
                if (!hasPublicSetter) continue;

                var propertyType = property.Type.ToDisplayString();

                var avaloniaWarnFix = template.Key == "Avalonia" ? "!" : string.Empty;

                _ = target.PropertyChangedMap.TryGetValue(property.Name, out var map)
                    ? sb.AppendLine(@$"            case ""{GetPropertyName(target, property)}"": {map}(GetValue({GetPropertyName(target, property)}Property){avaloniaWarnFix}); break;")
                    : sb.AppendLine(@$"            case ""{GetPropertyName(target, property)}"": {path}.{property.Name} = ({propertyType})GetValue({GetPropertyName(target, property)}Property){avaloniaWarnFix}; break;");
            }
        }

        return sb.ToString();
    }

    private static string GetCommandsChangesMap(XamlObject target)
    {
        var sb = new StringBuilder();

        foreach (var @event in target.Events)
        {
            var commandPropertyName = $"{@event.Name}Command";
            var delegateType = @event.Type as INamedTypeSymbol;
            var parametersCount = delegateType?.DelegateInvokeMethod?.Parameters.Length ?? 0;

            var parametersString = string.Join(", ", Enumerable.Range(0, parametersCount).Select(i => $"arg{i}"));
            var parametersObject = parametersCount <= 1
                ? parametersString
                : $"LiveChartsCore.Generators.CommandParameters.Create({parametersString})";

            _ = sb.AppendLine(@$"            case ""{commandPropertyName}"":
                if (!_setCommands.Contains(""{commandPropertyName}""))
                {{
                    _setCommands.Add(""{commandPropertyName}"");
                    _baseType.{@event.Name} += ({parametersString}) =>
                    {{
                        var commandParameter = {parametersObject};
                        if ({commandPropertyName} is null || !{commandPropertyName}.CanExecute(commandParameter)) return;
                        {commandPropertyName}.Execute(commandParameter);
                    }};
                }}
                break;");
        }

        return sb.ToString();
    }

    private static string GetPropertyName(XamlObject target, IPropertySymbol property)
    {
        var propertyName = property.Name;
        if (target.OverridenNames.TryGetValue(propertyName, out var overridenName))
            propertyName = overridenName;
        return propertyName;
    }

    private static string GetSeriesExtraGeneration(XamlObject target)
    {
        var isGauge = target.Type.Name.Contains("Gauge");

        var constraints = target.Type.TypeParameters
            .Select(p => (
                Argument: p,
                Constraints: string.Join(", ", p.ConstraintTypes.Select(c => c.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))))
            .ToArray();

        var sb = new StringBuilder();

        var previousConstraints = string.Join(", ", constraints.Select(x => x.Argument.Name));
        var originalConstraints = previousConstraints;

        var newLine = @"
";

        for (var i = constraints.Length - 1; i >= 0; i--)
        {
            var targetArgs = constraints.Skip(0).Take(i);

            var genericArgs = string.Join(", ", targetArgs.Select(x => x.Argument.Name));
            var newConstraints = string.Join(newLine, targetArgs
                .Where(x => x.Constraints is not null && x.Constraints.Length > 0)
                .Select(x => $"    where {x.Argument.Name} : {x.Constraints}{(x.Argument.HasConstructorConstraint ? ", new()" : string.Empty)}"));
            if (newConstraints.Length > 0)
                newConstraints = $"{newLine}{newConstraints}";

            var implemented = constraints.Skip(i).ToArray();
            foreach (var (arg, c) in implemented)
                previousConstraints = previousConstraints.Replace(arg.Name, target.SeriesArgs[arg.Name]);

            _ = sb.Append($@"
/// <inheritdoc cref=""{target.BasedOn.Name}{{{originalConstraints}}}"" />
public class {target.Type.OriginalDefinition.Name}{(genericArgs.Length > 0 ? $"<{genericArgs}>" : string.Empty)} : {target.Type.OriginalDefinition.Name}<{previousConstraints}>{newConstraints}
{{
    
}}
");

            previousConstraints = genericArgs;
        }

        return sb.ToString();
    }

#pragma warning disable format
    public static Dictionary<string, string> TypeConverters { get; } =
        new Dictionary<string, string>()
            .With("double[]",                                               "StringToDoubleArrayTypeConverter")
            .With("string[]",                                               "StringArrayTypeConverter")
            .With("LiveChartsCore.Drawing.Padding",                         "PaddingTypeConverter")
            .With("LiveChartsCore.Measure.Margin",                          "MarginTypeConverter")
            .With("LiveChartsCore.Drawing.LvcPoint",                        "PointTypeConverter")
            .With("LiveChartsCore.Drawing.LvcPointD",                       "PointDTypeConverter")
            .With("LiveChartsCore.Drawing.LvcColor",                        "HexToLvcColorTypeConverter")
            .With("LiveChartsCore.Drawing.LvcColor[]",                      "HexToLvcColorArrayTypeConverter")
            .With("LiveChartsCore.Painting.Paint",                          "HexToPaintTypeConverter")
            .With("System.Collections.Generic.IReadOnlyCollection<TModel>", "ValuesTypeConverter");
#pragma warning restore format

    private static Dictionary<string, string> With(
        this Dictionary<string, string> source,
        string type,
        string converter)
    {
        if (type.EndsWith("[]"))
        {
            var typeName = type.Substring(0, type.Length - 2);

            source.Add($"{typeName}[]", converter);
            source.Add($"{typeName}[]?", converter);
            source.Add($"System.Collections.Generic.IEnumerable<{typeName}>", converter);
            source.Add($"System.Collections.Generic.IEnumerable<{typeName}>?", converter);
            source.Add($"System.Collections.Generic.IList<{typeName}>", converter);
            source.Add($"System.Collections.Generic.IList<{typeName}>?", converter);
        }
        else
        {
            source.Add(type, converter);
            source.Add($"{type}?", converter);
        }

        return source;
    }

    private static string Concatenate<TItem>(
        IEnumerable<TItem> propertyToWrite,
        Func<TItem, string> syntaxBuilder,
        bool appendLine = true)
    {
        var sb = new StringBuilder();

        foreach (var item in propertyToWrite)
            _ = appendLine
                ? _ = sb.AppendLine(syntaxBuilder(item))
                : _ = sb.Append(syntaxBuilder(item));

        return sb.ToString();
    }
}
